@juangadm/pre-post 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -288
- package/assets/labels/post.png +0 -0
- package/assets/labels/pre.png +0 -0
- package/dist/attach.d.ts +59 -0
- package/dist/attach.d.ts.map +1 -0
- package/dist/attach.js +137 -0
- package/dist/attach.js.map +1 -0
- package/dist/baseline.d.ts +26 -1
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +92 -19
- package/dist/baseline.js.map +1 -1
- package/dist/bin/cli.js +40 -16
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +79 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +179 -38
- package/dist/browser.js.map +1 -1
- package/dist/commands/compare.d.ts.map +1 -1
- package/dist/commands/compare.js +5 -5
- package/dist/commands/compare.js.map +1 -1
- package/dist/commands/detect.d.ts +5 -0
- package/dist/commands/detect.d.ts.map +1 -1
- package/dist/commands/detect.js +3 -0
- package/dist/commands/detect.js.map +1 -1
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +21 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts +5 -0
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +167 -113
- package/dist/commands/pr.js.map +1 -1
- package/dist/comparison.d.ts +5 -0
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +5 -5
- package/dist/comparison.js.map +1 -1
- package/dist/compose.d.ts +72 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +237 -0
- package/dist/compose.js.map +1 -0
- package/dist/diff-worker.d.ts +1 -0
- package/dist/diff-worker.d.ts.map +1 -1
- package/dist/diff-worker.js +1 -0
- package/dist/diff-worker.js.map +1 -1
- package/dist/diff.d.ts +42 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +142 -24
- package/dist/diff.js.map +1 -1
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +5 -1
- package/dist/doctor.js.map +1 -1
- package/dist/drift.d.ts +29 -0
- package/dist/drift.d.ts.map +1 -0
- package/dist/drift.js +106 -0
- package/dist/drift.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/ffmpeg.d.ts +35 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +143 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/moments.d.ts +83 -0
- package/dist/moments.d.ts.map +1 -0
- package/dist/moments.js +170 -0
- package/dist/moments.js.map +1 -0
- package/dist/record.d.ts +69 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +223 -0
- package/dist/record.js.map +1 -0
- package/dist/report.d.ts +29 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +174 -42
- package/dist/report.js.map +1 -1
- package/dist/routes/imports.d.ts +21 -1
- package/dist/routes/imports.d.ts.map +1 -1
- package/dist/routes/imports.js +32 -2
- package/dist/routes/imports.js.map +1 -1
- package/dist/routes.d.ts +18 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +110 -29
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +48 -12
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +140 -18
- package/dist/run.js.map +1 -1
- package/dist/timeline.d.ts +46 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +80 -0
- package/dist/timeline.js.map +1 -0
- package/dist/types.d.ts +95 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/video.d.ts +19 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +109 -0
- package/dist/video.js.map +1 -0
- package/package.json +4 -2
- package/skill/SKILL.md +38 -10
package/README.md
CHANGED
|
@@ -1,330 +1,85 @@
|
|
|
1
1
|
# pre-post
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
changed, captures each one before and after, uploads the images to GitHub, and puts them at
|
|
5
|
-
the top of the PR description where a reviewer can judge them at a glance.
|
|
3
|
+
**See the change before you read the diff.**
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|---|---|---|
|
|
11
|
-
| 1 | `--before` | `--after` |
|
|
12
|
-
| 2 | `before` in `.pre-post.json` | the preview deployment for this commit |
|
|
13
|
-
| 3 | the production deployment for the base commit | a local dev server |
|
|
14
|
-
| 4 | whatever is on production now | |
|
|
15
|
-
| 5 | the base commit, served locally | |
|
|
16
|
-
|
|
17
|
-
Rows 2 to 4 need no dev environment at all, which is the point: a preview deployment and a
|
|
18
|
-
production URL are enough for anyone who can open the PR. Deployments come from the GitHub
|
|
19
|
-
Deployments API, so Vercel, Cloudflare Pages, Netlify and Render all work with no extra
|
|
20
|
-
token and nothing to configure; a host that records only a commit status is read from the
|
|
21
|
-
deployment bot's own PR comment instead. Row 4 covers repositories that do not deploy every
|
|
22
|
-
push to their default branch, so nothing is deployed at the fork point — it prints which
|
|
23
|
-
commit Pre actually came from rather than implying the base.
|
|
24
|
-
|
|
25
|
-
pre-post never guesses a baseline. If no deployment can be found it says so and tells you
|
|
26
|
-
the one flag that fixes it, because a baseline that is quietly the wrong site reads as 100%
|
|
27
|
-
changed on every route and looks exactly like a real result.
|
|
28
|
-
|
|
29
|
-
The last baseline needs no network at all: it checks the base commit into a throwaway
|
|
30
|
-
worktree and boots its dev script. That keeps pre-post working inside a sandboxed agent
|
|
31
|
-
container, a CI job, or behind an egress allowlist — and it compares against exactly what
|
|
32
|
-
the branch forked from, rendered in the same browser as the Post side.
|
|
33
|
-
|
|
34
|
-
> Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James Clements / Vercel Labs.
|
|
5
|
+
pre-post puts before/after screenshots of every page your branch touched at the top of the
|
|
6
|
+
pull request. One command finds the changed routes, captures Pre and Post, highlights what
|
|
7
|
+
moved, and updates the PR description. No setup, no pasting images.
|
|
35
8
|
|
|
36
9
|
```
|
|
37
10
|
$ npx -y @juangadm/pre-post@latest pr
|
|
38
11
|
|
|
39
12
|
Routes (nextjs-app, 41ms): /, /pricing
|
|
40
|
-
|
|
41
|
-
/pricing medium src/app/pricing/page.tsx imports src/components/ui/button.tsx (2 hops)
|
|
42
|
-
Capturing 8 screenshots (2 route(s) × 2 viewport(s)) ...
|
|
43
|
-
changed /pricing @ mobile (1.42%, 1811ms)
|
|
13
|
+
Capturing 4 screenshots (2 route(s) × 1 viewport(s)) ...
|
|
44
14
|
changed /pricing @ desktop (0.64%, 2036ms)
|
|
45
|
-
same / @ mobile (0.00%, 1990ms)
|
|
46
15
|
same / @ desktop (0.00%, 2211ms)
|
|
47
|
-
Publishing 10 image(s) to acme/web@pre-post-assets ...
|
|
48
16
|
Updated PR description: https://github.com/acme/web/pull/42
|
|
49
|
-
pre-post · PR #42 · 2 route(s) · 2 viewport(s) · 6.8s
|
|
50
|
-
/ desktop no change
|
|
51
|
-
/ mobile no change
|
|
52
|
-
/pricing desktop 0.64% changed
|
|
53
|
-
/pricing mobile 1.4% changed
|
|
54
|
-
Comment: https://github.com/acme/web/pull/42
|
|
55
17
|
```
|
|
56
18
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
1. **Routes.** Diffs the branch against the merge base with `main` — fetching that branch
|
|
60
|
-
first when the checkout does not have it, which is the normal shape in CI and in the
|
|
61
|
-
web/sandbox editors. When no shared history can be established it stops with one
|
|
62
|
-
sentence rather than reporting an empty diff, and `--base <ref>` names the commit
|
|
63
|
-
directly. Then it follows the import
|
|
64
|
-
graph: a change to `components/ui/button.tsx` marks every page that imports it. Next.js
|
|
65
|
-
App Router and Pages Router, Vite apps (React Router, file-based `src/pages`), and a
|
|
66
|
-
generic fallback. Monorepos are handled by picking the app that owns the changed files.
|
|
67
|
-
2. **Capture.** Playwright + Chromium headless shell. The page's own clock is held
|
|
68
|
-
still while it loads and then run forward by a fixed budget, so a page that animates
|
|
69
|
-
on a timer is photographed at the same frame on both sides however fast each host
|
|
70
|
-
answered. Reduced motion, animations finished, caret hidden, fonts and images settled,
|
|
71
|
-
layout stable, lazy content primed. 2x device scale, full page (capped at 2400 CSS
|
|
72
|
-
px), desktop (add mobile with `-r`).
|
|
73
|
-
All routes and viewports run concurrently.
|
|
74
|
-
3. **Diff.** Pure-JS pixel comparison in worker threads. Reports the percentage changed,
|
|
75
|
-
the bounding box, and a tight crop of the changed region. A route counts as changed when
|
|
76
|
-
the painted difference covers at least `minChangedArea` CSS pixels² (default 100, roughly
|
|
77
|
-
a third of a 16px icon) or at least `threshold` of the canvas — the first rule is what
|
|
78
|
-
decides on a page, the second on a small image.
|
|
79
|
-
4. **Layout shift.** A padding change near the top of a page moves everything below it, and
|
|
80
|
-
pixel diffing counts every moved pixel as changed: a change a designer would call
|
|
81
|
-
"slightly roomier" reads as most of the page repainted, and the crop is suppressed just
|
|
82
|
-
when it would be most useful. So the two sides are checked for a single vertical offset
|
|
83
|
-
first — how far the content moved, and from which row. When one is found, Pre is re-spaced
|
|
84
|
-
into Post's layout and the two are compared there: the crop comes from that pair, and the
|
|
85
|
-
comment says `Content shifted down 48px` instead of quoting a percentage. Rows Post gained
|
|
86
|
-
are left as background rather than skipped, so content inserted above the shift — the
|
|
87
|
-
banner that caused it — still reads as new rather than as nothing. The offset is only
|
|
88
|
-
accepted when it accounts for most of the difference across the rows both sides share.
|
|
89
|
-
The raw numbers are left alone: a move is a visual change, and reporting less of one would
|
|
90
|
-
hide it. Reflow, where content moves both across and down, has no single offset to find,
|
|
91
|
-
so it is reported the way it always was.
|
|
92
|
-
5. **Publish.** Images go to a `pre-post-assets` branch in the same repository via the GitHub
|
|
93
|
-
API, as one commit per run. Nothing is committed to the PR branch, no CI is triggered,
|
|
94
|
-
and the blob URLs render on private repos — a screenshot is visible to exactly whoever
|
|
95
|
-
can see the repository. `pre-post prune` removes images for PRs closed more than 90 days
|
|
96
|
-
ago, but note that it commits a deletion rather than rewriting history: the older commits
|
|
97
|
-
still hold the blobs, so a link handed out earlier keeps working. Treat anything captured
|
|
98
|
-
as permanent, and think twice before pointing pre-post at a preview holding real data.
|
|
99
|
-
See [Screenshot storage](#screenshot-storage) for how fast the branch grows.
|
|
100
|
-
6. **Describe.** The images go in a delimited block at the top of the PR description,
|
|
101
|
-
replaced in place on every run and leaving your own text untouched. Changed routes show
|
|
102
|
-
a Pre/Post crop with the full page collapsed underneath; unchanged routes fold into a
|
|
103
|
-
single line. A pure layout shift — nothing changed once the move is undone — has no
|
|
104
|
-
region to crop, so it shows the full pages directly under the `Content shifted down 40px.
|
|
105
|
-
Nothing else changed.` line, with no collapsed section repeating them. If the PR cannot be edited — a fork, a read-only token — it falls back to
|
|
106
|
-
one sticky comment.
|
|
19
|
+
## Get started
|
|
107
20
|
|
|
108
|
-
|
|
21
|
+
Pick one. None of them installs anything permanently.
|
|
109
22
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
### Claude Code — easiest
|
|
23
|
+
**In Claude Code**: install the skill once, then say `/pre-post` after a UI change.
|
|
113
24
|
|
|
114
25
|
```bash
|
|
115
26
|
npx skills add juangadm/pre-post -y
|
|
116
27
|
```
|
|
117
28
|
|
|
118
|
-
|
|
119
|
-
knowledge needed.
|
|
120
|
-
|
|
121
|
-
### Terminal
|
|
29
|
+
**In a terminal**: run it on a branch with an open PR.
|
|
122
30
|
|
|
123
31
|
```bash
|
|
124
32
|
npx -y @juangadm/pre-post@latest pr
|
|
125
33
|
```
|
|
126
34
|
|
|
127
|
-
|
|
35
|
+
**On every PR, automatically**: add the [GitHub Action](https://github.com/juangadm/pre-post/blob/main/docs/github-action.md). No tokens or
|
|
36
|
+
secrets needed.
|
|
128
37
|
|
|
129
|
-
###
|
|
38
|
+
### You'll need
|
|
130
39
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| **A GitHub token** | `gh auth login`, or set `GH_TOKEN`. Needs write access — it publishes images and edits the PR. |
|
|
135
|
-
| **An open PR** | Where the images go. Without one, pre-post prints the markdown to paste. |
|
|
40
|
+
- **Node 20+**
|
|
41
|
+
- **A GitHub token** with write access: `gh auth login`, or set `GH_TOKEN`
|
|
42
|
+
- **An open PR**: without one, pre-post prints the markdown for you to paste
|
|
136
43
|
|
|
137
|
-
|
|
44
|
+
The first run downloads a small browser (~80 MB). Stuck? Run
|
|
138
45
|
`npx -y @juangadm/pre-post@latest doctor`.
|
|
139
46
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
For a team, or for work done in cloud agent sessions: GitHub runs pre-post on each push to a
|
|
143
|
-
PR and posts with the job's own token. Nobody sets up anything per person, and there are no
|
|
144
|
-
secrets to add. Add `.github/workflows/pre-post.yml`:
|
|
145
|
-
|
|
146
|
-
```yaml
|
|
147
|
-
name: pre-post
|
|
148
|
-
on: pull_request
|
|
149
|
-
permissions: { contents: write, pull-requests: write }
|
|
150
|
-
concurrency: { group: 'pre-post-${{ github.event.pull_request.number }}', cancel-in-progress: true }
|
|
151
|
-
jobs:
|
|
152
|
-
pre-post:
|
|
153
|
-
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
154
|
-
runs-on: ubuntu-latest
|
|
155
|
-
steps:
|
|
156
|
-
- uses: actions/checkout@v4
|
|
157
|
-
with: { ref: '${{ github.event.pull_request.head.sha }}', fetch-depth: 0 }
|
|
158
|
-
- uses: juangadm/pre-post@v1
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
The runner builds the base commit and the PR's commit and serves both, the same way pre-post
|
|
162
|
-
does on a laptop with nothing deployed. No preview deployment is involved, so Vercel's
|
|
163
|
-
Deployment Protection never gets in the way.
|
|
164
|
-
|
|
165
|
-
- **Your app must start with its dev script without secrets.** If it needs environment
|
|
166
|
-
variables, give them to the job with `env:`.
|
|
167
|
-
- **pnpm or yarn** comes from the `packageManager` field in package.json. Without one, set
|
|
168
|
-
it up before the pre-post step (e.g. `pnpm/action-setup`), as any other job would.
|
|
169
|
-
- **Fork PRs are skipped:** their token cannot write, so there is nowhere to post.
|
|
170
|
-
- **Flags** go in `with: { args: '--responsive' }`.
|
|
171
|
-
- **The run's images**, including `sheet.png`, are kept as a workflow artifact even when
|
|
172
|
-
posting fails.
|
|
173
|
-
- **Pinned:** `@v1` runs the pre-post version released with that tag, never whatever npm
|
|
174
|
-
calls `latest`.
|
|
175
|
-
|
|
176
|
-
## Usage
|
|
47
|
+
## Everyday commands
|
|
177
48
|
|
|
178
49
|
```bash
|
|
179
|
-
pre-post pr
|
|
180
|
-
pre-post pr --
|
|
181
|
-
pre-post pr
|
|
182
|
-
pre-post pr --
|
|
183
|
-
pre-post pr --
|
|
184
|
-
pre-post
|
|
185
|
-
pre-post
|
|
186
|
-
|
|
187
|
-
pre-post https://acme.com http://localhost:3000 --routes /pricing # ad-hoc comparison
|
|
188
|
-
pre-post before.png after.png # diff two images
|
|
189
|
-
pre-post detect # which routes does this branch touch?
|
|
190
|
-
pre-post login https://staging.acme.com # sign in once; the session is reused
|
|
191
|
-
pre-post prune --days 90 # clean up the assets branch
|
|
192
|
-
pre-post doctor # browser, token, dev server, config
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
When something needs a human, the CLI exits with code 3 and one sentence saying what to do
|
|
196
|
-
(log in, start the dev server, pass `--before`). Re-running picks up where it left off.
|
|
197
|
-
|
|
198
|
-
Exit codes, so a script or an agent can branch on them:
|
|
199
|
-
|
|
200
|
-
| code | meaning |
|
|
201
|
-
|---|---|
|
|
202
|
-
| 0 | done; for `doctor`, `pre-post pr` can run |
|
|
203
|
-
| 1 | the run failed — every capture errored, or an unexpected error; for `doctor`, a required check failed |
|
|
204
|
-
| 2 | the arguments could not be parsed |
|
|
205
|
-
| 3 | something needs a human; the message says what |
|
|
206
|
-
|
|
207
|
-
`doctor` marks a check **FAIL** only when `pre-post pr` has no way to proceed without it —
|
|
208
|
-
the browser, a GitHub token, and being inside a git repository. Everything else prints as
|
|
209
|
-
**note**: no dev server running, or no `--before` saved, narrows *which* strategy a run
|
|
210
|
-
picks rather than stopping it, so those never change the exit code.
|
|
211
|
-
|
|
212
|
-
Results go into a delimited block at the top of the PR description, which re-runs replace in
|
|
213
|
-
place, leaving your own text untouched. If the PR cannot be edited — a fork, a read-only
|
|
214
|
-
token — it falls back to a single sticky comment.
|
|
215
|
-
|
|
216
|
-
## Screenshot storage
|
|
217
|
-
|
|
218
|
-
Every real run adds one commit to the `pre-post-assets` branch holding that run's images —
|
|
219
|
-
roughly 100–200 KB for a small PR, and again on every re-run. As a rough guide, 20 PRs a
|
|
220
|
-
week at 3 runs each is about 9 MB a week, or ~450 MB a year.
|
|
221
|
-
|
|
222
|
-
Two things to know before that adds up:
|
|
223
|
-
|
|
224
|
-
- **`prune` tidies the branch; it does not shrink the repository.** It commits a deletion,
|
|
225
|
-
so the images stay in history and every link already posted in a PR keeps working. The
|
|
226
|
-
repository's size only ever grows.
|
|
227
|
-
- **A plain `git clone` downloads every branch**, screenshots included. Teammates who never
|
|
228
|
-
look at them can skip the branch in an existing clone (git 2.29+):
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
git config --add remote.origin.fetch '^refs/heads/pre-post-assets'
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
A weekly prune keeps the branch's own listing short. As a scheduled workflow:
|
|
235
|
-
|
|
236
|
-
```yaml
|
|
237
|
-
# .github/workflows/pre-post-prune.yml
|
|
238
|
-
on:
|
|
239
|
-
schedule: [{ cron: '17 6 * * 1' }] # Mondays
|
|
240
|
-
workflow_dispatch:
|
|
241
|
-
permissions: { contents: write, pull-requests: read }
|
|
242
|
-
jobs:
|
|
243
|
-
prune:
|
|
244
|
-
runs-on: ubuntu-latest
|
|
245
|
-
steps:
|
|
246
|
-
- uses: actions/checkout@v4
|
|
247
|
-
- run: npx -y @juangadm/pre-post@latest prune --days 90
|
|
248
|
-
env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' }
|
|
50
|
+
pre-post pr # screenshot this branch's PR
|
|
51
|
+
pre-post pr --routes /pricing,/docs # only these pages
|
|
52
|
+
pre-post pr -r # desktop and mobile
|
|
53
|
+
pre-post pr --before https://acme.com # set the "before" site (remembered)
|
|
54
|
+
pre-post pr --dry-run # try it without posting
|
|
55
|
+
pre-post login https://staging.acme.com # sign in once for protected sites
|
|
56
|
+
pre-post doctor # check your setup
|
|
249
57
|
```
|
|
250
58
|
|
|
251
|
-
|
|
252
|
-
opened once they are that old. Actually reclaiming the space means rewriting the branch's
|
|
253
|
-
history, which also breaks the images in those older PRs — so it is left as your call.
|
|
254
|
-
|
|
255
|
-
## Configuration
|
|
256
|
-
|
|
257
|
-
Optional `.pre-post.json` in the repo root:
|
|
59
|
+
If pre-post needs something from you, it stops with one sentence saying exactly what.
|
|
258
60
|
|
|
259
|
-
|
|
260
|
-
{
|
|
261
|
-
"before": "https://acme.com",
|
|
262
|
-
"routes": ["/"],
|
|
263
|
-
"samples": { "/blog/[slug]": "/blog/hello-world" },
|
|
264
|
-
"viewports": ["desktop"],
|
|
265
|
-
"fullPage": true,
|
|
266
|
-
"maxHeight": 2400,
|
|
267
|
-
"scale": 2,
|
|
268
|
-
"threshold": 0.001,
|
|
269
|
-
"minChangedArea": 100,
|
|
270
|
-
"maxRoutes": 6,
|
|
271
|
-
"ignore": ["apps/docs"],
|
|
272
|
-
"headers": {},
|
|
273
|
-
"assetsBranch": "pre-post-assets",
|
|
274
|
-
"baselineSetup": "pnpm run build:packages"
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
`baselineSetup` runs in the app directory between the install and the dev server when the
|
|
279
|
-
baseline is built from source. Use it for whatever the app needs before it can boot — a
|
|
280
|
-
workspace build, a codegen step. In a turborepo it is inferred: `turbo run build
|
|
281
|
-
--filter=<app>^...`, which builds the packages the app imports and leaves the app to the
|
|
282
|
-
dev server. Setting it turns the guess off.
|
|
61
|
+
## How it decides what "before" is
|
|
283
62
|
|
|
284
|
-
|
|
285
|
-
|
|
63
|
+
Post (your branch) is the PR's preview deployment, or a local dev server. Pre is the
|
|
64
|
+
production site your branch forked from. When nothing is deployed, pre-post builds the base
|
|
65
|
+
commit itself, so it works in CI and in sandboxes too. It never guesses: if it can't find a
|
|
66
|
+
trustworthy baseline, it tells you which flag to pass.
|
|
286
67
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
the old one. Without this an auth-gated app rejects its own callbacks and every capture is a
|
|
290
|
-
loading skeleton. The names rewritten are the ones that can only mean the app's own address
|
|
291
|
-
— `BETTER_AUTH_URL`, `NEXTAUTH_URL`, `AUTH_URL`, `APP_URL`, `SITE_URL`, `CANONICAL_URL` and
|
|
292
|
-
their `_ORIGIN`/`_BASE_URL` forms, with a `NEXT_PUBLIC_` / `VITE_` / `PUBLIC_` prefix
|
|
293
|
-
optional. Generic names are left alone: `BASE_URL`, `SERVER_URL` and `PUBLIC_URL` are as
|
|
294
|
-
often a backend on another port or a path prefix, and `DATABASE_URL` is never an origin.
|
|
68
|
+
Screenshots are stored on a `pre-post-assets` branch in your own repo, so only people who can
|
|
69
|
+
see the repo can see them. Nothing is committed to your PR branch.
|
|
295
70
|
|
|
296
|
-
|
|
71
|
+
## Learn more
|
|
297
72
|
|
|
298
|
-
|
|
|
73
|
+
| | |
|
|
299
74
|
|---|---|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
307
|
-
## Library
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
import { runPr, captureScreenshot, diffImages, detectRoutesForRepo } from '@juangadm/pre-post';
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Development
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
pnpm install
|
|
317
|
-
pnpm build
|
|
318
|
-
pnpm test:unit
|
|
319
|
-
TEST_BROWSER=true pnpm test # needs a Chromium; npx playwright-core install chromium-headless-shell
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
pnpm and npm cannot share a `node_modules`. pnpm builds a symlinked tree that npm
|
|
323
|
-
cannot read, so `npm install` over it fails with `Cannot read properties of null
|
|
324
|
-
(reading 'edgesOut')`, which names none of that. Run `rm -rf node_modules
|
|
325
|
-
site/node_modules` before switching either way. npm also installs the CLI only —
|
|
326
|
-
`site/` is a pnpm workspace member, so use pnpm to work on the site.
|
|
75
|
+
| [How it works](https://github.com/juangadm/pre-post/blob/main/docs/how-it-works.md) | How Pre and Post are chosen, capture, diffing, layout shifts |
|
|
76
|
+
| [GitHub Action](https://github.com/juangadm/pre-post/blob/main/docs/github-action.md) | Run on every PR with no per-person setup |
|
|
77
|
+
| [Video: Moments](https://github.com/juangadm/pre-post/blob/main/docs/video.md) | Record interactions (menus, hovers, transitions) as synced Pre \| Post clips |
|
|
78
|
+
| [Reference](https://github.com/juangadm/pre-post/blob/main/docs/reference.md) | All commands, exit codes, `.pre-post.json`, environment variables |
|
|
79
|
+
| [Screenshot storage](https://github.com/juangadm/pre-post/blob/main/docs/storage.md) | How much space it uses and how to prune it |
|
|
80
|
+
| [Contributing](https://github.com/juangadm/pre-post/blob/main/CONTRIBUTING.md) | Build and test locally |
|
|
327
81
|
|
|
328
|
-
##
|
|
82
|
+
## Credits
|
|
329
83
|
|
|
330
|
-
|
|
84
|
+
Forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James
|
|
85
|
+
Clements / Vercel Labs. MIT licensed.
|
|
Binary file
|
|
Binary file
|
package/dist/attach.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put clips where GitHub will play them.
|
|
3
|
+
*
|
|
4
|
+
* A video plays inline in a PR only from GitHub's own attachment storage
|
|
5
|
+
* (`github.com/user-attachments/assets/…`); a file on the assets branch is a
|
|
6
|
+
* download link at best. Since 2026-09-01 `gh` uploads there with `--attach`
|
|
7
|
+
* (gh 2.99+, a person's login or a classic PAT — the Actions GITHUB_TOKEN is
|
|
8
|
+
* refused), rewriting a local reference in the text it posts into the
|
|
9
|
+
* attachment URL.
|
|
10
|
+
*
|
|
11
|
+
* The description itself is written by the same code as always, so the clips
|
|
12
|
+
* are uploaded on their own first: a temporary comment carries them up, their
|
|
13
|
+
* URLs are read back, and the comment is deleted — the flow vercel-labs'
|
|
14
|
+
* before-and-after settled on. Anything that goes wrong leaves the caller to
|
|
15
|
+
* fall back to the assets branch. The cost: PR subscribers are notified of a
|
|
16
|
+
* comment that is gone by the time they look. Passing `--attach` to the
|
|
17
|
+
* description edit itself would avoid it, but the sticky-comment fallback
|
|
18
|
+
* would still need this flow.
|
|
19
|
+
*/
|
|
20
|
+
import { GitHub } from './github.js';
|
|
21
|
+
import { MomentOutcome } from './types.js';
|
|
22
|
+
/** The first gh release whose `--attach` works on PRs and comments. */
|
|
23
|
+
export declare const MIN_GH_FOR_ATTACH: readonly [2, 99, 0];
|
|
24
|
+
export declare const UPDATE_GH_HINT = "Update GitHub CLI to 2.99 or newer (https://cli.github.com, or `brew upgrade gh`) to play videos inline in the PR.";
|
|
25
|
+
/** `gh version 2.101.0 (2026-09-20)` → [2, 101, 0]. */
|
|
26
|
+
export declare function parseGhVersion(output: string): number[] | null;
|
|
27
|
+
export declare function atLeast(version: number[], min: readonly number[]): boolean;
|
|
28
|
+
/** Whether this machine's gh can attach video, and if not the sentence that fixes it. */
|
|
29
|
+
export declare function ghAttachSupport(): {
|
|
30
|
+
ok: true;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/** The attachment URLs in a comment body, in the order they appear. */
|
|
36
|
+
export declare function attachmentUrls(body: string): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Upload `files` (all in one folder) as PR attachments. Returns file → URL.
|
|
39
|
+
* Throws when any of them does not come back as an attachment.
|
|
40
|
+
*/
|
|
41
|
+
export declare function uploadAttachments(gh: GitHub, ownerRepo: string, prNumber: number, files: string[], token?: string): Promise<Map<string, string>>;
|
|
42
|
+
export interface AttachOptions {
|
|
43
|
+
gh: GitHub;
|
|
44
|
+
ownerRepo: string;
|
|
45
|
+
/** No PR yet means nowhere to attach to. */
|
|
46
|
+
pr?: number;
|
|
47
|
+
token?: string;
|
|
48
|
+
log?: (msg: string) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Attach recorded clips to the PR where possible, marking each one inline.
|
|
52
|
+
* Returns the clips left for the assets branch, and the one sentence that
|
|
53
|
+
* would make them play when there are any.
|
|
54
|
+
*/
|
|
55
|
+
export declare function attachClips(clips: MomentOutcome[], opts: AttachOptions): Promise<{
|
|
56
|
+
linked: MomentOutcome[];
|
|
57
|
+
hint?: string;
|
|
58
|
+
}>;
|
|
59
|
+
//# sourceMappingURL=attach.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,qBAAsB,CAAC;AAErD,eAAO,MAAM,cAAc,uHAAuH,CAAC;AAKnJ,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAG9D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAK1E;AAED,yFAAyF;AACzF,wBAAgB,eAAe,IAAI;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAQ9E;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAErD;AAmBD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA4B9B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlI"}
|
package/dist/attach.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put clips where GitHub will play them.
|
|
3
|
+
*
|
|
4
|
+
* A video plays inline in a PR only from GitHub's own attachment storage
|
|
5
|
+
* (`github.com/user-attachments/assets/…`); a file on the assets branch is a
|
|
6
|
+
* download link at best. Since 2026-09-01 `gh` uploads there with `--attach`
|
|
7
|
+
* (gh 2.99+, a person's login or a classic PAT — the Actions GITHUB_TOKEN is
|
|
8
|
+
* refused), rewriting a local reference in the text it posts into the
|
|
9
|
+
* attachment URL.
|
|
10
|
+
*
|
|
11
|
+
* The description itself is written by the same code as always, so the clips
|
|
12
|
+
* are uploaded on their own first: a temporary comment carries them up, their
|
|
13
|
+
* URLs are read back, and the comment is deleted — the flow vercel-labs'
|
|
14
|
+
* before-and-after settled on. Anything that goes wrong leaves the caller to
|
|
15
|
+
* fall back to the assets branch. The cost: PR subscribers are notified of a
|
|
16
|
+
* comment that is gone by the time they look. Passing `--attach` to the
|
|
17
|
+
* description edit itself would avoid it, but the sticky-comment fallback
|
|
18
|
+
* would still need this flow.
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { spawn, spawnSync } from 'child_process';
|
|
24
|
+
/** The first gh release whose `--attach` works on PRs and comments. */
|
|
25
|
+
export const MIN_GH_FOR_ATTACH = [2, 99, 0];
|
|
26
|
+
export const UPDATE_GH_HINT = 'Update GitHub CLI to 2.99 or newer (https://cli.github.com, or `brew upgrade gh`) to play videos inline in the PR.';
|
|
27
|
+
const ATTACHMENT_URL = /https:\/\/github\.com\/user-attachments\/assets\/[\w-]+/g;
|
|
28
|
+
const UPLOAD_MARKER = '<!-- pre-post:upload (temporary; deleted once the clips are uploaded) -->';
|
|
29
|
+
/** `gh version 2.101.0 (2026-09-20)` → [2, 101, 0]. */
|
|
30
|
+
export function parseGhVersion(output) {
|
|
31
|
+
const m = /gh version (\d+)\.(\d+)\.(\d+)/.exec(output);
|
|
32
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
33
|
+
}
|
|
34
|
+
export function atLeast(version, min) {
|
|
35
|
+
for (let i = 0; i < min.length; i++) {
|
|
36
|
+
if ((version[i] ?? 0) !== min[i])
|
|
37
|
+
return (version[i] ?? 0) > min[i];
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Whether this machine's gh can attach video, and if not the sentence that fixes it. */
|
|
42
|
+
export function ghAttachSupport() {
|
|
43
|
+
const probe = spawnSync('gh', ['--version'], { encoding: 'utf-8' });
|
|
44
|
+
if (probe.error || probe.status !== 0) {
|
|
45
|
+
return { ok: false, reason: 'Install GitHub CLI 2.99 or newer (https://cli.github.com) to play videos inline in the PR.' };
|
|
46
|
+
}
|
|
47
|
+
const version = parseGhVersion(probe.stdout);
|
|
48
|
+
if (!version || !atLeast(version, MIN_GH_FOR_ATTACH))
|
|
49
|
+
return { ok: false, reason: UPDATE_GH_HINT };
|
|
50
|
+
return { ok: true };
|
|
51
|
+
}
|
|
52
|
+
/** The attachment URLs in a comment body, in the order they appear. */
|
|
53
|
+
export function attachmentUrls(body) {
|
|
54
|
+
return body.match(ATTACHMENT_URL) ?? [];
|
|
55
|
+
}
|
|
56
|
+
function runGh(args, cwd, token) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn('gh', args, {
|
|
59
|
+
cwd,
|
|
60
|
+
// The token pre-post already uses, so gh acts as the same identity.
|
|
61
|
+
env: token ? { ...process.env, GH_TOKEN: token } : process.env,
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
});
|
|
64
|
+
let out = '';
|
|
65
|
+
let err = '';
|
|
66
|
+
child.stdout.on('data', d => { out += d; });
|
|
67
|
+
child.stderr.on('data', d => { err += d; });
|
|
68
|
+
child.on('error', reject);
|
|
69
|
+
child.on('close', code => code === 0 ? resolve(out) : reject(new Error(err.trim().split('\n').pop() || `gh exited ${code}`)));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Upload `files` (all in one folder) as PR attachments. Returns file → URL.
|
|
74
|
+
* Throws when any of them does not come back as an attachment.
|
|
75
|
+
*/
|
|
76
|
+
export async function uploadAttachments(gh, ownerRepo, prNumber, files, token) {
|
|
77
|
+
if (!files.length)
|
|
78
|
+
return new Map();
|
|
79
|
+
const dir = path.dirname(files[0]);
|
|
80
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'pre-post-upload-'));
|
|
81
|
+
const bodyFile = path.join(scratch, 'body.md');
|
|
82
|
+
// `--attach` rewrites references to the files it is given, resolved from
|
|
83
|
+
// gh's working directory, so both use the same relative spelling.
|
|
84
|
+
const refs = files.map(f => `./${path.basename(f)}`);
|
|
85
|
+
fs.writeFileSync(bodyFile, [UPLOAD_MARKER, '', ...refs.map(r => ``), ''].join('\n'));
|
|
86
|
+
try {
|
|
87
|
+
const out = await runGh([
|
|
88
|
+
'pr', 'comment', String(prNumber), '--repo', ownerRepo, '--body-file', bodyFile,
|
|
89
|
+
...refs.flatMap(r => ['--attach', r]),
|
|
90
|
+
], dir, token);
|
|
91
|
+
const id = /#issuecomment-(\d+)/.exec(out)?.[1];
|
|
92
|
+
if (!id)
|
|
93
|
+
throw new Error('gh did not say which comment it posted');
|
|
94
|
+
const comment = await gh.request('GET', `/repos/${ownerRepo}/issues/comments/${id}`);
|
|
95
|
+
const urls = attachmentUrls(comment.body ?? '');
|
|
96
|
+
if (urls.length !== files.length) {
|
|
97
|
+
// Kept, not deleted: the comment is the only place the uploads are listed.
|
|
98
|
+
throw new Error(`expected ${files.length} attachment(s), GitHub returned ${urls.length}`);
|
|
99
|
+
}
|
|
100
|
+
// The attachment outlives the comment that carried it up.
|
|
101
|
+
await gh.request('DELETE', `/repos/${ownerRepo}/issues/comments/${id}`).catch(() => undefined);
|
|
102
|
+
return new Map(files.map((f, i) => [f, urls[i]]));
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Attach recorded clips to the PR where possible, marking each one inline.
|
|
110
|
+
* Returns the clips left for the assets branch, and the one sentence that
|
|
111
|
+
* would make them play when there are any.
|
|
112
|
+
*/
|
|
113
|
+
export async function attachClips(clips, opts) {
|
|
114
|
+
const log = opts.log ?? (() => undefined);
|
|
115
|
+
if (!opts.pr)
|
|
116
|
+
return { linked: clips, hint: 'Open the PR and re-run to play the videos inline.' };
|
|
117
|
+
const support = ghAttachSupport();
|
|
118
|
+
if (!support.ok) {
|
|
119
|
+
log(`Videos will be linked, not inline: ${support.reason}`);
|
|
120
|
+
return { linked: clips, hint: support.reason };
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const urls = await uploadAttachments(opts.gh, opts.ownerRepo, opts.pr, clips.map(m => m.file), opts.token);
|
|
124
|
+
for (const m of clips) {
|
|
125
|
+
m.videoUrl = urls.get(m.file);
|
|
126
|
+
m.inline = true;
|
|
127
|
+
}
|
|
128
|
+
log(`Attached ${clips.length} video(s) to PR #${opts.pr}.`);
|
|
129
|
+
return { linked: [] };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const hint = `GitHub did not take the videos as attachments (${err instanceof Error ? err.message : err}); they are linked instead. Inline video needs gh signed in as a person or with a classic token, not the Actions GITHUB_TOKEN.`;
|
|
133
|
+
log(hint);
|
|
134
|
+
return { linked: clips, hint };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.js","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAIjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAU,CAAC;AAErD,MAAM,CAAC,MAAM,cAAc,GAAG,oHAAoH,CAAC;AAEnJ,MAAM,cAAc,GAAG,0DAA0D,CAAC;AAClF,MAAM,aAAa,GAAG,2EAA2E,CAAC;AAElG,uDAAuD;AACvD,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,CAAC,GAAG,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxD,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAiB,EAAE,GAAsB;IAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,4FAA4F,EAAE,CAAC;IAC7H,CAAC;IACD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,iBAAiB,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACnG,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,KAAK,CAAC,IAAc,EAAE,GAAW,EAAE,KAAyB;IACnE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;YAC9B,GAAG;YACH,oEAAoE;YACpE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG;YAC9D,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAChI,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAU,EACV,SAAiB,EACjB,QAAgB,EAChB,KAAe,EACf,KAAc;IAEd,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC/C,yEAAyE;IACzE,kEAAkE;IAClE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,aAAa,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAClG,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC;YACtB,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ;YAC/E,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;SACtC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACf,MAAM,EAAE,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QACnE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAmB,KAAK,EAAE,UAAU,SAAS,oBAAoB,EAAE,EAAE,CAAC,CAAC;QACvG,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;YACjC,2EAA2E;YAC3E,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,MAAM,mCAAmC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,0DAA0D;QAC1D,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,SAAS,oBAAoB,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/F,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAsB,EAAE,IAAmB;IAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,mDAAmD,EAAE,CAAC;IAClG,MAAM,OAAO,GAAG,eAAe,EAAE,CAAC;IAClC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,GAAG,CAAC,sCAAsC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5D,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;IACjD,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAK,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5G,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YAAC,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAK,CAAC,CAAC;YAAC,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC;QAAC,CAAC;QAC3E,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,oBAAoB,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5D,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,kDAAkD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,gIAAgI,CAAC;QACxO,GAAG,CAAC,IAAI,CAAC,CAAC;QACV,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACjC,CAAC;AACH,CAAC"}
|
package/dist/baseline.d.ts
CHANGED
|
@@ -11,13 +11,24 @@
|
|
|
11
11
|
* The cost is time: a checkout, an install and a dev-server boot. So callers
|
|
12
12
|
* reach for it after a reachable deployment, not before one.
|
|
13
13
|
*/
|
|
14
|
+
import { ChildProcess } from 'child_process';
|
|
14
15
|
import { NeedsHumanError } from './errors.js';
|
|
15
16
|
import { Stopwatch } from './timings.js';
|
|
16
17
|
export interface LocalBaseline {
|
|
17
18
|
url: string;
|
|
18
19
|
/** Stop the dev server and delete the worktree. Safe to call twice. */
|
|
19
20
|
stop: () => Promise<void>;
|
|
21
|
+
/** Everything the dev server printed, so a failure can be explained with its own words. */
|
|
22
|
+
logFile?: string;
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Where a dev server's output goes: a file per server, never the terminal.
|
|
26
|
+
*
|
|
27
|
+
* It used to go nowhere (`stdio: 'ignore'`), so when a server answered 500
|
|
28
|
+
* there was no way to say why. A file rather than a pipe: the server is
|
|
29
|
+
* detached and must not stall when this process stops reading.
|
|
30
|
+
*/
|
|
31
|
+
export declare function serverLogFile(side: 'pre' | 'post', port: number): string;
|
|
21
32
|
interface PackageManager {
|
|
22
33
|
bin: string;
|
|
23
34
|
install: string[];
|
|
@@ -172,7 +183,7 @@ export declare function installDeps(pm: PackageManager, cwd: string, timeoutMs:
|
|
|
172
183
|
/**
|
|
173
184
|
* The install failed and nothing else can rescue this run.
|
|
174
185
|
*
|
|
175
|
-
* Its own error rather than a quiet null, for the reason docs/portability.md
|
|
186
|
+
* Its own error rather than a quiet null, for the reason docs/internal/portability.md
|
|
176
187
|
* §1 gives about base resolution: a run that compared nothing must not exit
|
|
177
188
|
* clean. A null here fell through to "no baseline", and on a repository with a
|
|
178
189
|
* configured production URL it fell through to comparing against that instead
|
|
@@ -257,6 +268,20 @@ export declare function reusableInstall(repoRoot: string, sha: string): Promise<
|
|
|
257
268
|
* removed so the normal install starts from a clean tree.
|
|
258
269
|
*/
|
|
259
270
|
export declare function reuseInstall(repoRoot: string, worktree: string, dirs: string[]): Promise<boolean>;
|
|
271
|
+
/** How long a dev server gets to exit on SIGTERM before it is killed. */
|
|
272
|
+
export declare const SERVER_STOP_TIMEOUT_MS = 5000;
|
|
273
|
+
/**
|
|
274
|
+
* SIGTERM the server's process group, wait for it to exit, then make sure.
|
|
275
|
+
*
|
|
276
|
+
* The wait is on the child itself — the package manager, which exits once the
|
|
277
|
+
* server under it has — because Node reaps it, so its exit is a fact. Asking
|
|
278
|
+
* the group with signal 0 was not: in a container whose PID 1 does not reap,
|
|
279
|
+
* dead members linger as zombies that still answer, and every stop waited out
|
|
280
|
+
* the full timeout. Once the child is gone, or `timeoutMs` has passed, the
|
|
281
|
+
* whole group is SIGKILLed, so nothing it started is left writing into a
|
|
282
|
+
* worktree about to be removed.
|
|
283
|
+
*/
|
|
284
|
+
export declare function stopGroup(child: ChildProcess, timeoutMs?: number): Promise<void>;
|
|
260
285
|
export interface BaselineOptions {
|
|
261
286
|
repoRoot: string;
|
|
262
287
|
/** Commit to serve. Omit to serve the working tree as it stands. */
|